/* Copyright 2011-2013 Google Inc.
 * Copyright 2013 mike wakerly <opensource@hoho.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * Project home page: https://github.com/mik3y/usb-serial-for-android
 */

package com.smartboard.usbserial;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.util.Log;

import com.smartboard.usbserial.driver.UsbSerialPort;
import com.smartboard.usbserial.util.HexDump;
import com.smartboard.usbserial.util.SerialInputOutputManager;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * read/write a single {@link UsbSerialPort} instance
 *
 */
public class SerialConsole {

    private final String TAG = SerialConsole.class.getSimpleName();

    private Context mContext;
    private UsbSerialPort mPort = null;
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    private SerialListener mSerialListener = null;

    private SerialInputOutputManager mSerialIoManager;

    public SerialConsole(Context context) {
        mContext = context;
    }

    public interface SerialListener {
        void onOpen();
        void onClose();
        void onReceiveDate(byte[] data);
    }

    public void setReceiveDataListener(SerialListener listener) {
        mSerialListener = listener;
    }

    public void open() {
        UsbSerialManager.init(mContext.getApplicationContext());
        UsbSerialManager.instance().updateDevice(new UsbSerialManager.DeviceUpdateListener() {
            @Override
            public void onDeviceUpdate(List<UsbSerialPort> entries) {
                // we use only the first port
                if(entries.size() > 0 && mPort != entries.get(0)) {
                    mPort = entries.get(0);
                    openPort();
                }
            }
        });
    }

    public void close() {
        stopIoManager();
        if(mPort != null) {
            try {
                mPort.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if(mSerialListener != null)
            mSerialListener.onClose();
        mPort = null;
    }

    public boolean writeData(byte[] data) {
        if(mPort == null)
            return false;
        try {
            mPort.write(data, 1000);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    private void openPort() {
        if(mPort == null)
            return;
        grantAutomaticPermission(mPort.getDriver().getDevice());

        UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);

        UsbDeviceConnection connection = usbManager.openDevice(mPort.getDriver().getDevice());
        if (connection == null) {
            Log.e(TAG, "Opening device failed");
            close();
            return;
        }

        try {
            mPort.open(connection);
            mPort.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
            mPort.setDTR(true);

            showStatus("CD  - Carrier Detect", mPort.getCD());
            showStatus("CTS - Clear To Send", mPort.getCTS());
            showStatus("DSR - Data Set Ready", mPort.getDSR());
            showStatus("DTR - Data Terminal Ready", mPort.getDTR());
            showStatus("DSR - Data Set Ready", mPort.getDSR());
            showStatus("RI  - Ring Indicator", mPort.getRI());
            showStatus("RTS - Request To Send", mPort.getRTS());

            writeData(new String("\r\n\r\n").getBytes());
        } catch (IOException e) {
            Log.e(TAG, "Error setting up device: " + e.getMessage(), e);
            close();
            return;
        }

        stopIoManager();
        startIoManager();

        if(mSerialListener != null)
            mSerialListener.onOpen();
    }

    // poll serial data
    private final SerialInputOutputManager.Listener mListener =
            new SerialInputOutputManager.Listener() {

        @Override
        public void onRunError(Exception e) {
            close();
            Log.d(TAG, "Runner stopped.");
        }

        @Override
        public void onNewData(byte[] data) {
            String message = "receive " + data.length + " bytes: \n"
                    + HexDump.dumpHexString(data) + "\n\n";
            Log.d(TAG,  message);
            if(mSerialListener != null)
                mSerialListener.onReceiveDate(data);
        }
    };

    private void showStatus(String theLabel, boolean theValue){
        String msg = theLabel + ": " + (theValue ? "enabled" : "disabled") + "\n";
        Log.d(TAG, "serial status: " + msg);
    }

    private void stopIoManager() {
        if (mSerialIoManager != null) {
            Log.i(TAG, "Stopping io manager ..");
            mSerialIoManager.stop();
            mSerialIoManager = null;
        }
    }

    private void startIoManager() {
        if (mPort != null) {
            Log.i(TAG, "Starting io manager ..");
            mSerialIoManager = new SerialInputOutputManager(mPort, mListener);
            mExecutor.submit(mSerialIoManager);
        }
    }

    private boolean grantAutomaticPermission(UsbDevice usbDevice) {
        try {
            PackageManager pkgManager = mContext.getPackageManager();
            ApplicationInfo appInfo=pkgManager.getApplicationInfo(mContext.getPackageName(), PackageManager.GET_META_DATA);

            Class serviceManagerClass=Class.forName("android.os.ServiceManager");
            Method getServiceMethod=serviceManagerClass.getDeclaredMethod("getService",String.class);
            getServiceMethod.setAccessible(true);
            android.os.IBinder binder=(android.os.IBinder)getServiceMethod.invoke(null, Context.USB_SERVICE);

            Class iUsbManagerClass=Class.forName("android.hardware.usb.IUsbManager");
            Class stubClass=Class.forName("android.hardware.usb.IUsbManager$Stub");
            Method asInterfaceMethod=stubClass.getDeclaredMethod("asInterface", android.os.IBinder.class);
            asInterfaceMethod.setAccessible(true);
            Object iUsbManager=asInterfaceMethod.invoke(null, binder);

            Log.d(TAG, appInfo.uid + " " + appInfo.processName + " " + appInfo.permission);
            final Method grantDevicePermissionMethod = iUsbManagerClass.getDeclaredMethod("grantDevicePermission", UsbDevice.class,int.class);
            grantDevicePermissionMethod.setAccessible(true);
            grantDevicePermissionMethod.invoke(iUsbManager, usbDevice,appInfo.uid);

            Log.d(TAG, "Method OK : " + binder + "  " + iUsbManager);
            return true;
        } catch(Exception e) {
            Log.e(TAG, "Error trying to assing automatic usb permission : ");
            e.printStackTrace();
            return false;
        }
    }

}
